package com.glsc.ngateway.flowable.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.ParallelGateway;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.transaction.Transactional;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Component
public class GatewayExecutionCompensationUtil {

    @Resource
    private RuntimeService runtimeService;
    @Resource
    private TaskService taskService;
    @Resource
    private RepositoryService repositoryService;
    @Resource
    private HistoryService historyService;


    @Resource
    @Qualifier("namedParameterJdbcTemplate")
    private NamedParameterJdbcTemplate namedParameterJdbcTemplateMysql;

    /**
     * Check and compensation.
     * 检测出异常的网关,并补偿网关的流程实例: 重建缺失flow的任务,并完成.
     *
     * @param processId the process id
     */
    @Transactional
    public void checkAndCompensation(String processId) {
        if (StrUtil.isBlank(processId)) {
            return;
        }
        log.info("【step】checkAndCompensation,start");
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();
        //1.判断是否存在网关执行实例  gw-ex
        String queryGwExSql = "select distinct b.ACT_ID_ from ACT_RU_EXECUTION a left join ACT_RU_ACTINST b on b.EXECUTION_ID_ = a.ID_ " +
                "where a.PROC_INST_ID_ = :processId and a.ACT_ID_ is not null and b.ACT_TYPE_ = 'parallelGateway'";
        Map<String, String> queryGwExMap = new HashMap<>();
        queryGwExMap.put("processId", processId);
        List<String> exGwIdList = namedParameterJdbcTemplateMysql.queryForList(queryGwExSql, queryGwExMap, String.class);
        log.info("【checkAndCompensation】执行实例-网关列表{}", exGwIdList);
        if (CollUtil.isEmpty(exGwIdList)) {
            return;
        }
        //2.查询 当前任务列表 用于判断是否存在需要补偿的网关
        Map<String, List<String>> target2sourceMap = getNodeRelationMap(pi);
        List<String> errGwList = new ArrayList<>();
        List<Task> list = taskService.createTaskQuery().processInstanceId(processId).list();
        log.info("【checkAndCompensation】任务列表{}", list);
        if (CollUtil.isNotEmpty(list)) {
            List<String> tkList = list.stream().map(Task::getTaskDefinitionKey).distinct().collect(Collectors.toList());
            log.info("【checkAndCompensation】去重任务列表{}", tkList);
            //检测是否缺失 gw-ex,并获取待补偿gw列表
            for (String gwid : exGwIdList) {
                int count = 0;
                if (tkList.stream().anyMatch(task -> isBelongtoSourceByFlowMap(target2sourceMap.get(gwid), task, target2sourceMap, count))) {
                    log.info("【checkAndCompensation】任务列表{}", gwid);
                    continue;
                }
                errGwList.add(gwid);
            }
            if (CollUtil.isEmpty(errGwList)) {
                return;
            }
        } else {
            errGwList.addAll(exGwIdList);
        }
        log.info("【checkAndCompensation】异常列表{}", errGwList);
        //3. 每次只处理一个   如果存在前后序关系,只处理前序,如果不存在,则只处理第一个.
        String firstGw = errGwList.get(0);
        if (errGwList.size() > 1) {
            for (int i = 1; i < errGwList.size(); i++) {
                int count = 0;
                if (isBelongtoSourceByFlowMap(target2sourceMap.get(firstGw), errGwList.get(i), target2sourceMap, count)) {
                    firstGw = errGwList.get(i);
                }
            }
        }
        log.info("【checkAndCompensation】最靠前的网关{}", firstGw);
        //4. 通过 firstGw  查询已有flow id
        String findGwFlowSql = "select distinct b.ACT_ID_ from ACT_RU_EXECUTION a left join ACT_RU_ACTINST b on b.EXECUTION_ID_ = a.ID_ " +
                "where a.PROC_INST_ID_ = :processId and a.ACT_ID_ =:gwid and b.ACT_TYPE_ like '%Flow'";
        Map<String, String> findGwFlowMap = new HashMap<>();
        findGwFlowMap.put("processId", processId);
        findGwFlowMap.put("gwid", firstGw);
        List<String> flowList = namedParameterJdbcTemplateMysql.queryForList(findGwFlowSql, findGwFlowMap, String.class);
        if (CollUtil.isEmpty(flowList)) {
            return;
        }
        //5. 排除掉已有flow id 获取需要补偿的任务task key
        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
        Map<String, FlowElement> flowElementMap = bpmnModel.getMainProcess().getFlowElementMap();
        String finalFirstGw = firstGw;
        FlowElement sf = flowElementMap.values().stream().filter(e -> e instanceof ParallelGateway).filter(e -> StrUtil.equals(e.getId(), finalFirstGw)).findFirst().get();
        List<SequenceFlow> incomingFlows = ((ParallelGateway) sf).getIncomingFlows();
        SequenceFlow sequenceFlow = incomingFlows.stream().filter(e -> !flowList.contains(e.getId())).findFirst().get();
        String sourTask = sequenceFlow.getSourceRef();

        //6.任务补偿  创建+完成 -> 自动生成gw-ex
        //todo 暂取会签人为admin
        List<Task> tasks = reBuildMulTask(List.of("admin"), sourTask, "补偿任务" + RandomUtil.randomInt(), processId);

        taskService.complete(tasks.get(0).getId());

        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processId).singleResult();
        //setVariable(绑定流程) , 任务完成后需要移除变量.
        if (ObjectUtil.isNull(historicProcessInstance.getEndTime())) {
            runtimeService.removeVariable(processId, "nrOfCompletedInstances");
            runtimeService.removeVariable(processId, "nrOfInstances");
        }
        log.info("【step】checkAndCompensation,end");
    }

    /**
     * 根据流程实例获取各节点->前置节点Map
     *
     * @param pi the process id
     * @return the Map
     */
    private Map<String, List<String>> getNodeRelationMap(ProcessInstance pi) {
        Map<String, List<String>> target2sourceMap = new HashMap<>();
        // 当前审批节点
        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
        Map<String, FlowElement> flowElementMap = bpmnModel.getMainProcess().getFlowElementMap();
        List<FlowElement> feList = flowElementMap.values().stream().filter(e -> e instanceof SequenceFlow).collect(Collectors.toList());
        for (FlowElement flowElement : feList) {
            String targetRef = ((SequenceFlow) flowElement).getTargetRef();
            String sourceRef = ((SequenceFlow) flowElement).getSourceRef();
            if (target2sourceMap.containsKey(targetRef)) {
                List<String> strings = target2sourceMap.get(targetRef);
                strings.add(sourceRef);
            } else {
                target2sourceMap.put(targetRef, CollectionUtil.newArrayList(sourceRef));
            }
        }
        return target2sourceMap;
    }

    private boolean isBelongtoSourceByFlowMap(List<String> sourceList, String sourceActivityId, Map<String, List<String>> target2sourceMap, int count) {
        if (CollUtil.isEmpty(sourceList) || count > 200) {
            return false;
        }
        Set<String> set = new HashSet<>();
        for (int i = 0; i < sourceList.size(); i++) {
            if (sourceList.get(i).equals(sourceActivityId)) {
                return true;
            }
            if (sourceList.get(i).equals("startEvent") || sourceList.get(i).equals("subProcessStartEvent")) {
                continue;
            }
            set.addAll(target2sourceMap.get(sourceList.get(i)));
        }
        log.info("isBelongtoSourceByFlowMap--sourceList:" + CollUtil.join(set, ","));
        count++;
        return isBelongtoSourceByFlowMap(new ArrayList<>(set), sourceActivityId, target2sourceMap, count);
    }


    /**
     * Re build mul task list.
     * 注: 该方法只适用于 无中生有!  会自建 根执行实例
     *
     * @param assigneeList the assignee list
     * @param taskKey      the task key
     * @param taskName     the task name
     * @param processId    the process id
     * @return the list
     */
    public List<Task> reBuildMulTask(List<String> assigneeList, String taskKey, String taskName, String processId) {
        log.info("reBuildMulTask:参数assigneeList:{}", assigneeList);
        log.info("reBuildMulTask:参数taskKey:{}", taskKey);
        log.info("reBuildMulTask:参数taskName:{}", taskName);
        log.info("reBuildMulTask:参数processId:{}", processId);
        if (CollUtil.isEmpty(assigneeList)) {
            return new ArrayList<>();
        }
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();
        String processDefinitionId = pi.getProcessDefinitionId();
        //转化为task
        List<Task> taskList = new ArrayList<>();
        assigneeList.forEach(ur -> {
            Task task = taskService.createTaskBuilder()
                    .id(UUID.fastUUID().toString())
                    .name(taskName)
                    .taskDefinitionKey(taskKey)
                    .tenantId("amprod")
                    .assignee(ur)
                    .create();
            taskService.saveTask(task);
            taskList.add(task);
        });


        String rootExId = UUID.fastUUID().toString();
        // 1. 插入根执行实例  insert ACT_RU_EXECUTION
        String insertRootExSql = "insert into ACT_RU_EXECUTION (ID_, REV_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_, ACT_ID_, IS_ACTIVE_, IS_CONCURRENT_, " +
                "IS_SCOPE_, IS_EVENT_SCOPE_, IS_MI_ROOT_, PARENT_ID_, SUPER_EXEC_, ROOT_PROC_INST_ID_, SUSPENSION_STATE_, TENANT_ID_, NAME_, START_ACT_ID_, START_TIME_, " +
                "START_USER_ID_, IS_COUNT_ENABLED_, EVT_SUBSCR_COUNT_, TASK_COUNT_, JOB_COUNT_, TIMER_JOB_COUNT_, SUSP_JOB_COUNT_, DEADLETTER_JOB_COUNT_, VAR_COUNT_, " +
                "ID_LINK_COUNT_, CALLBACK_ID_, CALLBACK_TYPE_) values (:uid, 1, :processId, NULL, :pdid, :tk, true, false, false, false, true, :parid, NULL, :processId," +
                " 1, 'amprod', NULL, NULL, :dt, NULL, true, 0, 1, 0, 0, 0, 0, 0, 0, NULL, NULL)";
        Map<String, Object> insertExMap = new HashMap<>();
        insertExMap.put("processId", processId);
        insertExMap.put("pdid", processDefinitionId);
        insertExMap.put("tk", taskKey);
        insertExMap.put("dt", new Date());
        insertExMap.put("parid", processId);
        insertExMap.put("uid", rootExId);
        namedParameterJdbcTemplateMysql.update(insertRootExSql, insertExMap);

        // 2. taskList->exList  && insert ACT_RU_EXECUTION

        taskList.forEach(task -> {
            log.info("开始补偿数据 并 更新 ACT_HI_TASKINST,ACT_RU_TASK");
            String exId = UUID.fastUUID().toString();
            String actId = UUID.fastUUID().toString();

            insertExMap.put("parid", rootExId);
            insertExMap.put("uid", exId);
            // 插入 common ex, 对应task
            String insertCommonExSql = "insert into ACT_RU_EXECUTION (ID_, REV_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_, ACT_ID_, IS_ACTIVE_, IS_CONCURRENT_, " +
                    "IS_SCOPE_, IS_EVENT_SCOPE_, IS_MI_ROOT_, PARENT_ID_, SUPER_EXEC_, ROOT_PROC_INST_ID_, SUSPENSION_STATE_, TENANT_ID_, NAME_, START_ACT_ID_, " +
                    "START_TIME_, START_USER_ID_, IS_COUNT_ENABLED_, EVT_SUBSCR_COUNT_, TASK_COUNT_, JOB_COUNT_, TIMER_JOB_COUNT_, SUSP_JOB_COUNT_, DEADLETTER_JOB_COUNT_," +
                    " VAR_COUNT_, ID_LINK_COUNT_, CALLBACK_ID_, CALLBACK_TYPE_) values (:uid, 1, :processId, NULL, :pdid, :tk, true, false, false, false, false, :parid," +
                    " NULL, :processId, 1, 'amprod', NULL, NULL, :dt, NULL, true, 0, 1, 0, 0, 0, 0, 0, 0, NULL, NULL)";
            namedParameterJdbcTemplateMysql.update(insertCommonExSql, insertExMap);

            // 插入 hi act, 对应task
            String insertHiActSql = "INSERT INTO ACT_HI_ACTINST (ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_," +
                    " ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TENANT_ID_) VALUES (:actId, 1, :pdid, :processId, :exId, :tk, :tid, null," +
                    " :tkname, 'userTask', :assignee, :dt, null, null, null, 'amprod')";
            Map<String, Object> insertActMap = new HashMap<>();
            insertActMap.put("processId", processId);
            insertActMap.put("pdid", processDefinitionId);
            insertActMap.put("tk", taskKey);
            insertActMap.put("tkname", taskName);
            insertActMap.put("assignee", task.getAssignee());
            insertActMap.put("dt", new Date());
            insertActMap.put("tid", task.getId());
            insertActMap.put("exId", exId);
            insertActMap.put("actId", actId);
            namedParameterJdbcTemplateMysql.update(insertHiActSql, insertActMap);

            // 插入 ru act, 对应task
            String insertRuActSql = "INSERT INTO ACT_RU_ACTINST (ID_, REV_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, " +
                    "END_TIME_, DURATION_, DELETE_REASON_, TENANT_ID_) VALUES (:actId, 1, :pdid, :processId, :exId, :tk, null, null, null, 'userTask', :assignee, :dt, null, null, null, 'amprod')";
            namedParameterJdbcTemplateMysql.update(insertRuActSql, insertActMap);

            //更新 ru_task hi_task :关联流程
            Map<String, Object> updateTaskMap = new HashMap<>();
            updateTaskMap.put("processId", processId);
            updateTaskMap.put("pdid", processDefinitionId);
            updateTaskMap.put("assignee", task.getAssignee());
            updateTaskMap.put("exId", exId);
            updateTaskMap.put("tid", task.getId());

            // !!!尽量保证update sql 晚于 insert
            int interval = 1;
            int taskid = this.getTaskId(task.getId());
            while (taskid == 0 && interval < 10) {
                taskid = this.getTaskId(task.getId());
                interval *= 2;
                ThreadUtil.safeSleep(interval * 1000);
            }
            if (taskid == 0) {
                log.error("重建节点任务异常,flowable_api(taskService.saveTask)未完成task的创建!!!");
                throw new RuntimeException("重建节点任务异常,flowable_api(taskService.saveTask)未完成task的创建!!!");
            }

            log.info("补偿sql:update ACT_RU_TASK set PROC_INST_ID_=:processId , PROC_DEF_ID_=" + updateTaskMap.get("pid") + " ,EXECUTION_ID_=" + updateTaskMap.get("exId") + " where ID_=" + updateTaskMap.get("tid") + "");
            log.info("补偿sql:update ACT_HI_TASKINST set PROC_INST_ID_=:processId , PROC_DEF_ID_=" + updateTaskMap.get("pid") + " ,EXECUTION_ID_=" + updateTaskMap.get("exId") + " where ID_=" + updateTaskMap.get("tid") + "");
            namedParameterJdbcTemplateMysql.update("update ACT_RU_TASK set PROC_INST_ID_=:processId , PROC_DEF_ID_=:pdid ,EXECUTION_ID_=:exId where ID_=:tid ", updateTaskMap);
            namedParameterJdbcTemplateMysql.update("update ACT_HI_TASKINST set PROC_INST_ID_=:processId , PROC_DEF_ID_=:pdid ,EXECUTION_ID_=:exId where ID_=:tid ", updateTaskMap);

            //补充流程节点定义内的变量   setVariableLocal(绑定任务,但是报错)   setVariable(绑定流程)
            taskService.setVariable(task.getId(), "nrOfCompletedInstances", 1);
            taskService.setVariable(task.getId(), "nrOfInstances", 1);
        });

        return taskService.createTaskQuery().processInstanceId(processId).taskDefinitionKey(taskKey).list();
    }

    public Integer getTaskId(String tid) {
        Map<String, Object> argMap = new HashMap<>();
        argMap.put("tid", tid);
        return namedParameterJdbcTemplateMysql.queryForObject("SELECT count(1) FROM ACT_RU_TASK WHERE ID_=:tid", argMap, Integer.class);
    }

}

